iT邦幫忙

2021 iThome 鐵人賽

DAY 4
0
Modern Web

用30天更加認識 React.js 這個好朋友系列 第 4

Day4-React Hook 篇-認識 useRef & useImperativeHandle

  • 分享至 

  • xImage
  •  

今天要介紹的是可以用來操作 DOM 元素的 useRef 及和它有關的 hook useImperativeHandle。

功用和特性

ref 是一個包含 current 屬性的 JS 物件,它具有以下功用和特性:

  1. 可以操作 DOM 元素(ex: 管理 focus、選擇文字)
  2. ref 可使用 mutate 的寫法,state 不能
  3. 更新 ref.current 的值時,不會讓元件重新渲染,而且在元件經過下次渲染後不會像一般變數一樣重置,也就是可以在多次渲染之間儲存資訊,所以可以依據此特性去發揮其功用。但相對的也不建議用 ref.current 去儲存會呈現在畫面上的資訊。

例如可以用來避免元件第一次 render 時,useEffect 內的程式碼執行:

react-hooks: skip first run in useEffect

import { useState, useRef, useEffect } from "react";

export default function App() {
  const [count, setCount] = useState(0);

  const isFirstRun = useRef(true);
  useEffect(() => {
    // 第一次執行 useEffect 就直接 return 掉,不執行後面的程式碼
    if (isFirstRun.current) {
      isFirstRun.current = false;
      return;
    }

    console.log("Effect was run");
  });

  return (
    <div>
      <p>Clicked {count} times</p>
      <button
        onClick={() => {
          setCount(count + 1);
        }}
      >
        Click Me
      </button>
    </div>
  );
}

補充-注意事項

不要在元件渲染期間去讀和寫 ref 的值。避免的原因是為了保持元件 pure,當前版本的 React 也許還不會出現問題,但未來 React 版本升級就有可能會有影響。

function MyComponent() {
  // ...
  // 避免改寫 ref 的值
  myRef.current = 123;
  // ...
  // 避免讀取 ref 的值
  return <h1>{myOtherRef.current}</h1>;
}

語法和範例

useRef 只會回傳一個值,這個值是一個有 current 屬性的物件,這個 ref.current 可以進行 mutable 的操作。

import React, { useEffect, useRef } from 'react';

const App = () => {
  const h1Ref = useRef(); // 定義一個 ref
	
  useEffect(() => {
    console.log(h1Ref.current); // 使用 ref 名字.current 取出 dom 元素
  }, [])
	
	// 綁定 ref 到 dom 元素上	
  return <h1 ref={h1Ref}>Hello World!</h1>
}
export default App;

focus input 輸入框

import { useState, useRef, useEffect } from "react";

export default function App() {
  const inputRef = useRef();

  const clickHandler = () => {
    inputRef.current.focus();
  };

  return (
    <>
      <input type="text" ref={inputRef} />
      <button onClick={clickHandler}>Focus</button>
    </>
  );
}

程式碼範例(codesandbox)

inputRef.current.focus();

參考 https://zh-hant.reactjs.org/docs/refs-and-the-dom.html

2021/10/22 補充

如果要取得 ref 取到的 DOM 元素底下的子元素,可以加上 children,如範例所示:

useEffect(() => {
    console.log(tableRef.current); // table
    console.log(tableRef.current.children[0]); // thead
    console.log(tableRef.current.children[1]); // tbody
    console.log(tableRef.current.children[0].children); // thead tr
    ...
}, [])

forwardRef

將 ref 傳遞到子元件,讓子元件可以取到父元件的 DOM。

範例

App.js

import { useRef } from "react";
import Child from "./Child";

const App = () => {
  const textInput = useRef(null);

  function handleClick() {
    textInput.current.focus();
  }
  return (
    <div>
      <Child ref={textInput} />
      <input type="button" value="Focus the text input" onClick={handleClick} />
    </div>
  );
};

export default App;

Child.js

import { forwardRef } from "react";

const Child = forwardRef((props, ref) => {
  console.log(ref); // <input type="text"></input>

  return <input type="text" ref={ref} />;
});

export default Child;

程式碼範例(codesandbox)

useImperativeHandle

此 hook 會和 forwardRef 一起使用,可以將子元件內的 ref 內置的方法或是值傳遞到母元件,此 hook 較不常使用。

語法

useImperativeHandle(ref, createHandle, [deps])
第二個參數是一個函式,會把裡面的東西綁定到 ref 上,第三個參數作用同 useEffect 的第二個參數,當值改變時重新執行第二個參數

範例

在範例中,透過 useImperativeHandle 將 verify 和 validate 兩個函式從 TextInput 元件傳到 App 元件

程式碼範例(codesandbox)

Callback Ref

於 2025/01/21 補充:

根據 React 官網提到 ref 可以傳入的初始值可以是任何型別:

The value you want the ref object’s current property to be initially. It can be a value of any type. This argument is ignored after the initial render.

那如果傳入的值是一個 callback 函式,並將該 callback 函式傳遞給 React Element 的 ref 屬性時,該 callback 函式將在以下情況下被觸發:

  1. 元件第一次渲染,React 會調用該 callback 函式,並將該 DOM 元素作為參數傳入回呼函式
  2. 元件重新渲染,同第一點情況
  3. 元件隱藏(當 DOM 元素從 DOM 樹中移除),傳入 null 作為 callback 函式的參數

官網:React will call the ref callback with the DOM element when the component mounts, and call it with null when it unmounts. Refs are guaranteed to be up-to-date before componentDidMount or componentDidUpdate fires.

所以舉個例子,若第一次渲染時,以下元件會打印出 "Ref set:" HTMLDivElement 和 "Mounted:" HTMLDivElement。

import React, { useRef, useEffect } from "react";

function App() {
  const callbackRef = useRef(null);

  useEffect(() => {
    if (callbackRef.current) {
      console.log("Mounted:", callbackRef.current);
    }

    return () => {
      if (callbackRef.current) {
        console.log("Unmounted:", callbackRef.current);
      }
    };
  }, []);

  const setRef = (element) => {
    if (element) {
      console.log("Ref set:", element); // 元件被掛載時
    } else {
      console.log("Ref cleared:", element); // 元件被卸載時
    }
    callbackRef.current = element; // 自行管理的引用
  };

  return <div ref={setRef}>Hello, Callback Ref!</div>;
}

export default App;

如此一來,能取到當前的 DOM 元素去做一些操作。

使用這個特性另外要注意幾個點:

  1. 確保 Callback ref 在設定新的 DOM 元素時先清理舊的引用,避免 memory leak 或意外行為。
  2. 搭配 useCallback 避免重新建立 callback 函式

參考資料

React Ref Callback:最佳实践

練習題

試著用 Callback Ref 解決 6. useHover()


上一篇
Day3-React Hook 篇-認識 useEffect
下一篇
Day5-React Hook 篇-認識 useContext
系列文
用30天更加認識 React.js 這個好朋友33
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言